package com.ant.backstage.config.shiro;

import com.ant.backstage.config.jwt.JwtToken;
import com.ant.backstage.facade.code.ApiReturnCode;
import com.ant.backstage.facade.dto.UserDTO;
import com.ant.backstage.facade.enums.UserStatusEnum;
import com.ant.backstage.facade.exception.BackstageBusinessException;
import com.ant.backstage.facade.model.Role;
import com.ant.backstage.facade.model.User;
import com.ant.backstage.facade.validation.UserConstant;
import com.ant.backstage.service.RegisterAndLoginService;
import com.ant.backstage.service.RoleService;
import com.ant.backstage.utils.RedisUtil;
import com.ant.backstage.utils.ioc.ApplicationContextUtil;
import com.ant.backstage.utils.jwt.JwtUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.util.Strings;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.stream.Collectors;


public class JwtRealm extends AuthorizingRealm {

    private static Logger logger = LogManager.getLogger(JwtRealm.class);
    @Resource
    RegisterAndLoginService registerAndLoginService;
    @Resource
    RoleService roleService;

    /**
     * 使用自定义的密码/token对比匹配器
     */
    public JwtRealm() {
        JwtCredentialsMatcher jwtCredentialsMatcher = new JwtCredentialsMatcher();
        // 将对应的匹配器设置到Realm中
        this.setCredentialsMatcher(jwtCredentialsMatcher);
    }


    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        UserDTO user = (UserDTO) principals.getPrimaryPrincipal();
        // 从数据库查询角色和权限信息
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        Set<String> permissions = registerAndLoginService.findPermissionsByUid(user.getUid()).stream().collect(Collectors.toSet());
        //查用户角色
        List<Role> roles = roleService.selectRolesByUid(user.getUid());
        Set<String> collect = roles.stream().distinct().map(Role::getRolename).collect(Collectors.toSet());
        authorizationInfo.setRoles(collect);
        authorizationInfo.setStringPermissions(permissions);
        return authorizationInfo;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) token;
        if (!Strings.isEmpty(jwtToken.getUid())) {
            HttpServletResponse httpResponse = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
            RedisUtil redisUtil = ApplicationContextUtil.getBean(RedisUtil.class);
            boolean b = redisUtil.hasKey(jwtToken.getUid() + UserConstant.USER_TOKEN_SUFFIX);
            if (!b && jwtToken.getToken() != null) {
                String refreshToken = JwtUtil.getClaimByKey(jwtToken.getToken(), "refreshToken");
                if (Optional.ofNullable(refreshToken).isPresent()) {
                    if (refreshToken.equals("N")) {
                        httpResponse.setHeader("Authorization", null);
                        logger.warn("用户认证信息,被清除");
                        return null;
                    }
                    // 场景 当管理员清除了用户的redis认证信息 此时如果不拦截这会再次生成redis缓存，造成无限续签 refreshToken N/Y 续签/登录接口
                }
            }

            RegisterAndLoginService registerAndLoginService = ApplicationContextUtil.getBean(RegisterAndLoginService.class);
             /*
            注意第一个参数必须是与token.getPrincipal()，否则在remove缓存时的key，会与put的时候的key不一样
            从而导致退出的时候无法remove掉缓存。

            principle是身份信息，简单的可以放username，网上很多人将User对象作为身份信息。
            不管是用username还是user对象作为principle，必须保证这里返回的SimpleAuthenticationInfo()的第一个参数
            和token.getPrincipal()一致，否则就会出现上面说的问题。详情看源码：AuthenticatingRealm的【两个】
            getAuthenticationCacheKey()方法

            但是将user作为principle（存入redis时会作为key），会遇到redis key的序列化和反序列化的问题，而且
            一个对象作为redis key不合适。一般redis key我们都尽量使用string，value对象可以使用jdk序列化或者
            json序列化。但我个人不建议使用json，用json无非是为了可视化，意义不大，但是由于java是强类型语言，
            反序列化的时候可能就会出问题
             */
            Map<String, String> claims = new HashMap<>();
            claims.put("uid", jwtToken.getUid().split("_")[0]);
            claims.put("refreshToken", "Y");
            claims.put("time", String.valueOf(System.currentTimeMillis()));
            // 生成jwt
            String jwt = registerAndLoginService.generateJwt(claims);
            // 根据token中的username去数据库核对信息，返回用户信息，并封装称SimpleAuthenticationInfo给Matcher去校验
            User user = registerAndLoginService.findByUid(jwtToken.getUid().split("_")[0]);
            if (!Optional.ofNullable(user).isPresent()) {
                return null;
            }
            //检查用户状态
            checkUserStatus(user);
            UserDTO userPo = new UserDTO().setUphone(user.getUphone()).setUname(user.getUname()).setUid(user.getUid())
                    .setPwd(user.getPwd()).setJwtSecret(user.getJwtSecret()).setCreatedatetime(user.getCreatedatetime())
                    .setToken(jwt).setAddressid(user.getAddressid()).setAge(user.getAge());
            httpResponse.setHeader("Authorization", jwt);
            // 身份信息可以在登录成功之后通过subject.getPrinciple()取出
            return new SimpleAuthenticationInfo(userPo,
                    new MyByteSource(user.getJwtSecret()),
                    this.getName());
        }
        return null;
    }

    /**
     * 检查用户状态
     * 如果用户状态异常，则抛出对应的错误
     *
     * @param //status 用户状态
     * @throws AuthenticationException 用户状态错误信息
     */
    private void checkUserStatus(User user) throws AuthenticationException {

        UserStatusEnum byCode = UserStatusEnum.getByCode(user.getStatus());
        if (byCode == null) {
            logger.error("不存在用户状态 user={}", user);
            throw new BackstageBusinessException(ApiReturnCode.HTTP_ERROR.getMessage(), ApiReturnCode.HTTP_ERROR.getCode());
        }
        switch (byCode) {
            case USER_STATE_FREEZE:
                logger.error("当前用户已被冻结 user={}", user);
                throw new LockedAccountException(ApiReturnCode.USER_STATE_FREEZE.getMessage());
            case USER_STATE_LOGOUT:
                logger.error("当前用户已被注销 user={}", user);
                throw new UnknownAccountException(ApiReturnCode.USER_STATE_LOGOUT.getMessage());
        }
    }
    /*
     *  shiro 认证异常
     *  1. ConcurrentAccessException    当认证请求时，该帐户已经处于认证完成的状态（例如：已登录）时，如果系统被配置成阻止重复登录时就会抛出该异常。禁止并发访问
     *
     *  2. CredentialsException  当用户提交的凭证有问题，抛出这个异常。比如说密码错误等。
     *
     *  3. DisabledAccountException 当尝试认证已经因为某种原因已经被Ban掉的帐号时抛出这个异常。 一些对于安全性要求比较高的系统，这种行为值得注意。
     *
     *  4. ExcessiveAttemptsException
     *  当系统被配置成固定的认证请求次数时，如果失败的认证请求次数超过这个数值时，就会抛出这个异常。针对这种穷举，各个系统是不一样的，大部分系统会临时或永久的冻结该帐户来防止进步的尝试。
     *
     *  5. ExpiredCredentialsException  当系统在认证的过程中确定用户提交的凭证已经过期且不允许再登录时，会抛出此异常。
     *
     *  6. IncorrectCredentialsException 当认证请求中提交的凭证与实际的凭证不符合时，抛出此异常。
     *
     * 7. LockedAccountException
     *  当请求认证的用户已经被明确标记为锁定状态时抛出这个异常。比如过多的失败尝试导致该用户被锁定。DisabledAccountException，是一种特定的DisabledAccountException
     *
     * 8. UnknownAccountException 当请求认证的帐号在系统中不存在时，抛出此异常。
     *
     *  9. UnauthenticatedException 当用户还没有进行认证时，就尝试执行需要授权的动作时，会抛出此异常。
     *
     *  10. UnauthorizedException 当尝试访问无权访问的资源时，抛出此异常。捕获此异常时，可以重定向到提示画面，记入日志，冻结帐号等，取决于系统的安全要求。
     *
     *  11. HostUnauthorizedException 继承自UnauthorizedException，当指定的客户端（IP，MAC地址等）不允许访问系统，或者是客户端虽然能够访问系统但是在尝试访问未授权的资源时，会抛出此异常。
     */


//    /**
//     * 设置加密方式
//     */
//    @PostConstruct
//    public void initCredentialsMatcher() {
//        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//        //采用SHA-512方式加密
//        hashedCredentialsMatcher.setHashAlgorithmName("SHA-512");
//        //设置加密次数
//        hashedCredentialsMatcher.setHashIterations(1024);
//        //true加密用的hex编码，false用的base64编码
//        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(false);
//        setCredentialsMatcher(hashedCredentialsMatcher);
//    }

}